RAII技术(资源获取即初始化——Resource Acquisition Is Initialization)
编写程序我们经常会使用new或者mallco来向系统申请内存,但我们也会可能忘记使用delete去释放他们,而这种不释放手动申请的资源的后果是十分严重(内存溢出 memory overflow)。这时候RAII技术应运而生。
RAII惯用法是在Bjarne Stroustrup的《C++程序设计语言(第3版)》一书中有讲到:
使用局部对象管理资源的技术通常称为“资源获取就是初始化”。这种通用技术依赖于构造函数和析构函数的性质以及它们与异常处理的交互作用
总结RAII技术就类似以下几点:
- 将将要申请的资源封装到类内(构造函数&析构函数)。
- 通过类的本地实例使用资源(类的对象)
- 当对象超出作用域(scope)时,资源将自动释放(系统自动调用析构函数)。
这保证了无论在资源使用期间发生什么,它最终都会被释放(不管是由于正常返回、销毁包含的对象,还是引发异常)。
RAII的本质内容是用对象代表资源,把管理资源的任务转化为管理对象的任务,将资源的获取和释放与对象的构造和析构对应起来,从而确保在对象的生存期内资源始终有效,对象销毁时资源必被释放;这种技术还能令代码更加简洁(如果手动申请了大量的资源,无穷无尽的delete语句着实令人头疼)。
例子
1 | void Func() |
如同标准做法,在申请了resource内存资源后,我们必须要对他进行释放。
但是凡事都有例外,可能在函数内发生了一些小插曲:
1 | void Func() |
我们在未释放掉resource的情况下就结束了函数(或者是程序抛出了异常导致函数结束)。
要注意的是,和申请的局部变量不同,函数中使用了new而不delete,通常是不会释放的空间的:内存被new出来以后,有一个指针指向这个空间。malloc 或者 new申请的空间是在”堆”上的,平时我们都是用声明变量来申请空间的,此时申请到的空间是”栈”上的。
不管是在子函数中,或是在主函数中,都必须有一次delete来释放这个空间,如果没有做delete,即使退出程序,也无法回收这段内存了,内存就被泄露了。
又或者我们有这样的代码:1
2
3
4
5
6
7
8
9
10
11
12
13void Func()
{
int *resource1= new int[10];
int *resource2= new int[9];
int *resource3= new int[666];
//.........
...//申请了成吨的资源空间
delete[] resource3;
delete[] resource2;
delete[] resource1;
//.........
...//根据需要释放某些内存(或者全部释放)
}
如果绩效根据代码量来计算(什么破公司!),那么这样的代码确实很有用。但是这看起来十分的令人头疼,毕竟实际情况指针变量名可不是123这样排序的。
这时候我们可以利用RAII技术——将将要申请的资源封装在类内:
1 | template<class sizetype> |
我们通常会禁止类对象之间进行相互拷贝或者拷贝构造,因为此类的对象代表一种资源,不具有复制的意义存在。
在运用了RAII之后,我们就不再害怕函数内发生错误而导致提前的结束了。
使用我们的Resource类来管理资源
1 | int main() |
至此,我们的资源已经如同局部对象一样:自动申请,自动释放。
同样的方法我们也可以运用到对文件的操作:fopen(),fclose()上 或者 scope lock (局部锁技术)等等对内存的把握也一样严格的技术上,RAII会帮到我们许多。